From dc4db405d2991d3ec6a114f3b08d3fccd057d3ee Mon Sep 17 00:00:00 2001
From: Wolfgang Müller
Date: Thu, 13 Feb 2025 17:52:16 +0100
Subject: frontend: Migrate to Svelte 5

---
 frontend/src/routes/comics/[id]/+page.svelte | 177 +++++++++++++--------------
 1 file changed, 82 insertions(+), 95 deletions(-)

(limited to 'frontend/src/routes/comics/[id]/+page.svelte')

diff --git a/frontend/src/routes/comics/[id]/+page.svelte b/frontend/src/routes/comics/[id]/+page.svelte
index cfc5840..48c588f 100644
--- a/frontend/src/routes/comics/[id]/+page.svelte
+++ b/frontend/src/routes/comics/[id]/+page.svelte
@@ -2,12 +2,14 @@
 	import { beforeNavigate } from '$app/navigation';
 	import { updateComics } from '$gql/Mutations';
 	import { comicQuery } from '$gql/Queries';
-	import { comicEquals } from '$gql/Utils';
-	import { UpdateMode, type FullComicFragment, type UpdateComicInput } from '$gql/graphql';
-	import { initReaderContext } from '$lib/Reader';
-	import { initScraperContext } from '$lib/Scraper';
-	import { initSelectionContext } from '$lib/Selection';
-	import { setTabContext } from '$lib/Tabs';
+	import { omitIdentifiers, type OmitIdentifiers } from '$gql/Utils';
+	import {
+		UpdateMode,
+		type FullComicFragment,
+		type PageFragment,
+		type UpdateComicInput
+	} from '$gql/graphql';
+	import { comicPending } from '$lib/Form';
 	import { toastFinally } from '$lib/Toasts';
 	import { preventOnPending } from '$lib/Utils';
 	import BookmarkButton from '$lib/components/BookmarkButton.svelte';
@@ -21,155 +23,140 @@
 	import ComicForm from '$lib/forms/ComicForm.svelte';
 	import Gallery from '$lib/gallery/Gallery.svelte';
 	import PageView from '$lib/reader/PageView.svelte';
-	import Reader from '$lib/reader/Reader.svelte';
+	import Reader, { initReaderContext } from '$lib/reader/Reader.svelte';
 	import ComicScrapeForm from '$lib/scraper/ComicScrapeForm.svelte';
+	import { initScraperContext } from '$lib/scraper/Scraper.svelte';
+	import { initSelectionContext } from '$lib/selection/Selection.svelte';
 	import ComicDelete from '$lib/tabs/ComicDelete.svelte';
 	import ComicDetails from '$lib/tabs/ComicDetails.svelte';
 	import Tab from '$lib/tabs/Tab.svelte';
 	import Tabs from '$lib/tabs/Tabs.svelte';
 	import SelectionControls from '$lib/toolbar/SelectionControls.svelte';
 	import { getContextClient } from '@urql/svelte';
-	import type { PageData } from './$types';
+	import { untrack } from 'svelte';
+	import type { PageProps } from './$types';
 
+	let { data }: PageProps = $props();
 	const client = getContextClient();
 	const reader = initReaderContext();
-	const selection = initSelectionContext();
 	const scraper = initScraperContext();
-	const tabContext = setTabContext({
-		tabs: {
-			details: { title: 'Details' },
-			edit: { title: 'Edit' },
-			scrape: { title: 'Scrape' },
-			deletion: { title: 'Delete' }
-		},
-		current: 'details'
-	});
+	const selection = initSelectionContext<PageFragment>('Page', (p) => p.path);
 
-	export let data: PageData;
-	$: result = comicQuery(client, { id: data.id });
-
-	let comic: FullComicFragment;
-	let original: Readonly<FullComicFragment>;
-	let updatePartial = false;
-
-	$: $result, update();
-	$: pending = !comicEquals(comic, original);
-	$: $tabContext.tabs.edit.badge = pending;
-
-	function update() {
-		if (!$result.stale && $result.data?.comic.__typename === 'FullComic') {
-			original = $result.data.comic;
-			if (updatePartial) {
-				comic.pages = structuredClone(original.pages);
-				comic.favourite = original.favourite;
-				comic.bookmarked = original.bookmarked;
-				comic.organized = original.organized;
-				comic.updatedAt = original.updatedAt;
-				updatePartial = false;
-			} else {
-				comic = structuredClone(original);
-			}
-
-			$reader.pages = original.pages;
-			$selection.view = comic.pages;
-			$scraper.selector = undefined;
-		}
+	let comic: FullComicFragment | undefined = $state();
+	let input: OmitIdentifiers<FullComicFragment> | undefined = $state();
+	let updateInput = $state(true);
+
+	function invalidateInput() {
+		updateInput = true;
 	}
 
-	function toggle(field: keyof Omit<UpdateComicInput, 'cover'>) {
-		updateComics(client, { ids: comic.id, input: { [field]: !comic[field] } })
-			.then(() => (updatePartial = true))
-			.catch(toastFinally);
+	function submit(input: UpdateComicInput) {
+		updateComics(client, { ids: data.id, input }).then(invalidateInput).catch(toastFinally);
 	}
 
-	function updateComic(event: CustomEvent<UpdateComicInput>) {
-		updateComics(client, { ids: comic.id, input: event.detail }).catch(toastFinally);
+	function toggle(field: keyof Pick<UpdateComicInput, 'bookmarked' | 'favourite' | 'organized'>) {
+		if (!comic) return;
+		updateComics(client, { ids: data.id, input: { [field]: !comic[field] } }).catch(toastFinally);
 	}
 
-	function updateCover(event: CustomEvent<number>) {
-		updateComics(client, { ids: comic.id, input: { cover: { id: event.detail } } })
-			.then(() => (updatePartial = true))
-			.catch(toastFinally);
+	function updateCover(id: number) {
+		updateComics(client, { ids: data.id, input: { cover: { id } } }).catch(toastFinally);
 	}
 
 	function removePages() {
 		updateComics(client, {
-			ids: comic.id,
-			input: { pages: { ids: $selection.ids, options: { mode: UpdateMode.Remove } } }
+			ids: data.id,
+			input: { pages: { ids: selection.ids, options: { mode: UpdateMode.Remove } } }
 		})
-			.then(() => {
-				updatePartial = true;
-				$selection = $selection.clear();
-			})
+			.then(selection.clear)
 			.catch(toastFinally);
 	}
 
 	beforeNavigate((navigation) => preventOnPending(navigation, pending));
+	let result = $derived(comicQuery(client, { id: data.id }));
+	let pending = $derived(comicPending(comic, input));
+
+	$effect(() => {
+		if (!$result.stale) {
+			untrack(() => {
+				if ($result.data?.comic.__typename === 'FullComic') {
+					comic = $result.data.comic;
+
+					if (updateInput) {
+						input = omitIdentifiers($result.data.comic);
+						updateInput = false;
+					}
+
+					reader.pages = comic.pages;
+					selection.view = comic.pages;
+					scraper.reset();
+				}
+			});
+		}
+	});
 </script>
 
-<Head section="Comic" title={original?.title} />
+<Head section="Comic" title={comic?.title} />
 
-{#if comic}
+{#if comic && input}
 	<Grid>
 		<header>
 			<Titlebar
-				title={original.title}
-				subtitle={original.originalTitle}
-				bind:favourite={comic.favourite}
-				on:favourite={() => toggle('favourite')}
+				title={comic.title}
+				subtitle={comic.originalTitle}
+				favourite={comic.favourite}
+				onfavourite={() => toggle('favourite')}
 			/>
 		</header>
 
 		<aside>
-			<Tabs>
-				<Tab id="details">
-					<ComicDetails comic={original} />
+			<Tabs badges={{ edit: pending }}>
+				<Tab initial id="details" title="Details">
+					<ComicDetails {comic} />
 				</Tab>
-				<Tab id="edit">
+				<Tab id="edit" title="Edit">
 					<div class="flex flex-col gap-4">
 						<div class="flex gap-2 text-sm">
 							<SelectionControls page>
-								<RemovePageButton on:click={removePages} />
+								<RemovePageButton onclick={removePages} />
 							</SelectionControls>
-							<div class="grow" />
-							<BookmarkButton bookmarked={comic.bookmarked} on:click={() => toggle('bookmarked')} />
-							<OrganizedButton organized={comic.organized} on:click={() => toggle('organized')} />
+							<div class="grow"></div>
+							<BookmarkButton bookmarked={comic.bookmarked} onclick={() => toggle('bookmarked')} />
+							<OrganizedButton organized={comic.organized} onclick={() => toggle('organized')} />
 						</div>
-						<ComicForm bind:comic on:submit={updateComic}>
+						<ComicForm bind:input {submit}>
 							<div class="flex gap-2">
-								<div class="grow" />
-								<SubmitButton active={pending} />
+								<div class="grow"></div>
+								<SubmitButton {pending} />
 							</div>
 						</ComicForm>
 					</div>
 				</Tab>
-				<Tab id="scrape">
-					<ComicScrapeForm {comic} />
+				<Tab id="scrape" title="Scrape">
+					<ComicScrapeForm {comic} onupsert={invalidateInput} />
 				</Tab>
-				<Tab id="deletion">
+				<Tab id="deletion" title="Delete">
 					<ComicDelete {comic} />
 				</Tab>
 			</Tabs>
 		</aside>
 
 		<main class="overflow-auto">
-			<Gallery
-				pages={comic.pages}
-				on:open={(e) => ($reader = $reader.open(e.detail))}
-				on:cover={updateCover}
-			/>
+			<Gallery pages={comic.pages} open={reader.open} {updateCover} />
 		</main>
 	</Grid>
 
 	<Reader>
 		<PageView layout={comic.layout} direction={comic.direction} />
-		<svelte:fragment slot="sidebar">
-			<ComicForm bind:comic on:submit={updateComic}>
-				<div class="flex justify-end gap-2">
-					<SubmitButton active={pending} />
-				</div>
-			</ComicForm>
-		</svelte:fragment>
+		{#snippet sidebar()}
+			{#if input}
+				<ComicForm bind:input {submit}>
+					<div class="flex justify-end gap-2">
+						<SubmitButton {pending} />
+					</div>
+				</ComicForm>
+			{/if}
+		{/snippet}
 	</Reader>
 {:else}
 	<Guard {result} />
-- 
cgit v1.2.3-2-gb3c3

